/* eslint-disable @typescript-eslint/no-explicit-any */
import { Component } from './component';
import { context, noWindowFound } from './context';
import { BaseElement, CSSStyle, ChildType, ElementProps, EventHandler, EventMap, JSXClass, ObjectProps } from './types';

function expect(what: string, found: string): never {
	throw new Error(`invalid css token: expect ${what}, but found: ${found}'`);
}

function camelToKebabCase(str: string): string {
	return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
}

function isIterable(obj: any): obj is Iterable<unknown> {
	return typeof obj === 'object' && obj != null && typeof obj[Symbol.iterator] === 'function';
}

export function setCSS(element: BaseElement, object: CSSStyle | string) {
	const style = element.style;
	if (typeof object === 'string') {
		object = parseCss(object);
		return;
	}
	Object.entries(object).forEach(([key, value]) => {
		const k = key.startsWith('--') ? key : camelToKebabCase(key);
		if (value !== null || value !== undefined) {
			const v = (value + '').trim();
			if (v.endsWith('!important')) {
				style?.setProperty(k, v.slice(0, -10).trim(), 'important');
			} else {
				style?.setProperty(k, v + '');
			}
		} else {
			style?.removeProperty(k);
		}
	});
}

export function parseCss(str: string): CSSStyle {
	const properties: Record<string, string> = {};
	const regex =
		/((?<=(?:^|;)\s*)(?:--[^:]+|[a-z]+(?:-[a-z]+)*)(?=\s*:))|(\s*:\s*)|(\s*;\s*)|((?<=:\s*)(?:'[^']*(?<!')'|"[^"]*(?<!")"|[^;:'"]+)+)|([^:]+)/g;
	let m: RegExpExecArray | null = null;
	let key: string | undefined;
	let value: string | undefined;
	let state = 0;
	while ((m = regex.exec(str))) {
		// key - 1
		// : - 2
		// ; - 3
		// value - 4
		const word = m[0];
		const type = m.indexOf(word, 1);
		if (state === 0) {
			if (type === 1) {
				if (word.startsWith('--')) {
					key = word;
				} else {
					key = word.replace(/-([a-z])/g, (s, c) => c.toUpperCase());
				}
				state = 1;
			} else if (type === 3) {
				// do nothing
			} else expect('property name', word);
		} else if (state === 1) {
			if (type !== 2) expect('":"', word);
			state = 2;
		} else if (state === 2) {
			if (type === 4) {
				value = word;
				if (key) {
					properties[key] = value;
				}
			} else if (type === 3) {
				// do nothing
			} else {
				expect('css value', word);
			}
			state = 0;
		}
	}
	return properties;
}

function append(parent: ParentNode, children: ChildType) {
	if (isIterable(children)) {
		for (const child of children) {
			if (typeof child === 'string' || child instanceof context.Node) {
				parent.append(child);
			} else if (child !== undefined) {
				parent.append(child + '');
			}
		}
	} else if (typeof children === 'string' || children instanceof context.Node) {
		parent.append(children);
	} else if (children !== undefined) {
		parent.append(children + '');
	}
}

export function appendAll(parent: ParentNode, children: ChildType | ChildType[] | undefined) {
	if (children === undefined) return;
	if (Array.isArray(children)) {
		for (const child of children) {
			append(parent, child);
		}
	} else {
		append(parent, children);
	}
}

function getPropertyDescriptor(obj: object, prop: string) {
	let currentObj = obj;
	while (currentObj) {
		const descriptor = Object.getOwnPropertyDescriptor(currentObj, prop);
		if (descriptor) {
			return descriptor;
		}
		currentObj = Object.getPrototypeOf(currentObj);
	}
	return undefined;
}

export function setAttr(element: BaseElement, key: string, value: unknown) {
	if (value === null || value === undefined) {
		element.removeAttribute(key);
	} else if (typeof value === 'boolean') {
		if (value) {
			element.setAttribute(key, key);
		} else {
			element.removeAttribute(key);
		}
	} else {
		element.setAttribute(key, value + '');
	}
}

export function setAttrs(element: BaseElement, attrs: Record<string, unknown>) {
	Object.entries(attrs).forEach(([k, v]) => {
		setAttr(element, k, v);
	});
}

export function jsxGen<T extends BaseElement>(elem: T, props: ElementProps<T>) {
	Object.keys(props).forEach((k) => {
		const v = props[k];
		if (k.startsWith('on')) {
			let capture = false;
			if (k.endsWith('Capture') && k !== 'onGotPointerCapture' && k !== 'onLostPointerCapture') {
				capture = true;
				k = k.substring(0, k.length - 7);
			}
			if (typeof v === 'function') {
				elem.addEventListener(k.substring(2).toLowerCase(), v as EventHandler<keyof EventMap, T>, { capture });
			} else if (typeof v === 'string') {
				elem.setAttribute(k.toLowerCase(), v);
			}
		} else if (k === 'style') {
			if (v && typeof v === 'string') {
				setCSS(elem, parseCss(v));
			} else if (v && v instanceof Object) {
				setCSS(elem, v);
			}
		} else if (k === 'class') {
			if (typeof v === 'string') {
				elem.classList.add(...v.split(/\s+/).filter((s) => !!s));
			} else if (Array.isArray(v)) {
				elem.classList.add(...v.map((s) => s.trim()).filter((s) => !!s));
			}
		} else if (k === 'ref' || k === 'children') {
			//
		} else if (v !== undefined) {
			if (k in elem) {
				if (isPropertyReadOnly(elem, k)) {
					setAttr(elem, k, v);
				} else {
					(elem as unknown as Record<string, unknown>)[k] = v;
				}
			} else {
				setAttr(elem, k, v);
			}
		}
	});
	appendAll(elem, props.children);
}

let objProps: WeakMap<object, Map<string, boolean>> | undefined;

function isPropertyReadOnly(obj: object, prop: string): boolean {
	let propMap = objProps?.get(obj);
	let readonly = propMap?.get(prop);
	if (readonly === undefined) {
		const descriptor = getPropertyDescriptor(obj, prop);
		readonly = descriptor ? descriptor.writable === false || (!!descriptor.get && !descriptor.set) : false;
		if (objProps && !propMap) {
			propMap = new Map();
			objProps.set(obj, propMap);
		}
		if (propMap) {
			propMap.set(prop, readonly);
		}
		return readonly;
	} else {
		return readonly;
	}
}

export function isComponentClass<T>(
	func: object
): func is new (props: ObjectProps<T>, children: ChildType) => JSXClass<T> {
	return Object.prototype.isPrototypeOf.call(Component, func);
}

export function isHTMLElementClass<T extends HTMLElement>(func: object): func is new () => T {
	return Object.prototype.isPrototypeOf.call(context.HTMLElement, func);
}

export function isSVGElementClass<T extends HTMLElement>(func: object): func is new () => T {
	return Object.prototype.isPrototypeOf.call(context.SVGElement, func);
}

export function isMathMLElementClass<T extends HTMLElement>(func: object): func is new () => T {
	return Object.prototype.isPrototypeOf.call(context.MathMLElement, func);
}

export function render(renderFunc: () => Node, selector?: string) {
	if (!context) {
		throw new Error('no window found');
	}
	try {
		objProps = new WeakMap();
		const result = renderFunc();
		if (selector) {
			context.document.querySelector(selector)?.replaceChildren(result);
		} else {
			context.document.body.replaceChildren(result);
		}
	} finally {
		objProps = undefined;
	}
}

export function unsafe(html: string): DocumentFragment {
	if (!context) {
		noWindowFound();
	}
	const template = context.document.createElement('template');
	template.innerHTML = html;
	return template.content;
}

export function cls(cls?: string[] | string, ...extra: (string | undefined)[]): string[] {
	if (cls === undefined) {
		return extra.filter((v): v is string => !!v);
	} else if (typeof cls === 'string') {
		return extra.concat(cls.split(/\s+/)).filter((v): v is string => !!v);
	} else {
		return extra.concat(cls).filter((v): v is string => !!v && v.trim().length > 0);
	}
}
